 using System.Collections.Generic;

namespace CoAnimator
{
    public delegate void FireEvent();

    /// <summary>
    /// Abstract class for animators. Think of this as a puppet-master for movable objects.
    /// </summary>
    public abstract class Animator<T> : IAnimator
    {
        double mNextFrameCountdown;
        double mTimeElapsed;
        IEnumerator<double> mAnimation;
        Dictionary<string, FireEvent> mEvents;
        Dictionary<string, bool> mFlags;
        bool mPaused;
        bool mTimed;
        T mPuppet;

        /// <summary>
        /// If you make the class extending this an inner class, you can use this to manipulate
        /// the private methods of the outer class. x_x
        /// </summary>
        public T Puppet { get { return mPuppet; } }

        protected Animator(T pPuppet)
        {
            mPaused = true;
            mEvents = new Dictionary<string, FireEvent>();
            mFlags = new Dictionary<string, bool>();
            mPuppet = pPuppet;
            Play();
        }

        #region events and flags

        /// <summary>
        /// attach a delegate to be called when a named event is fired
        /// </summary>
        /// <param name="pName">name of event</param>
        /// <param name="pFire">delegate to fire</param>
        public void ListenFor(string pName, FireEvent pFire)
        {
            mEvents[pName] = pFire;
        }

        /// <summary>
        /// removes a delegate from a named event
        /// </summary>
        /// <param name="pName">name of event to defuse</param>
        /// <returns></returns>
        public bool Forget(string pName)
        {
            return mEvents.Remove(pName);
        }

        /// <summary>
        /// check if a flag has been raised
        /// </summary>
        /// <param name="pFlag">name of flag</param>
        /// <returns>flag state</returns>
        public bool Flagged(string pFlag)
        {
            return mFlags.ContainsKey(pFlag) && mFlags[pFlag];
        }

        /// <summary>
        /// raise a flag
        /// </summary>
        /// <param name="pFlag">name of flag</param>
        public void Flag(string pFlag)
        {
            mFlags[pFlag] = true;
        }

        /// <summary>
        /// lower a flag
        /// </summary>
        /// <param name="pFlag">name of flag</param>
        public void Unflag(string pFlag)
        {
            mFlags[pFlag] = false;
        }

        #endregion

        #region play controls

        /// <summary>
        /// Start the animation playing from where it was left off
        /// </summary>
        public void Play()
        {
            Play(mTimeElapsed);
        }

        /// <summary>
        /// Start the animation playing from a specified time
        /// </summary>
        /// <param name="pTime">Time in seconds to begin animation from</param>
        public void Play(double pTime)
        {
            mTimeElapsed = pTime;
            mPaused = false;

            if (mAnimation == null)
            {
                mAnimation = Animate().GetEnumerator();
                mNextFrameCountdown = -pTime;
            }

            Fire("start");
        }

        /// <summary>
        /// Pause animation
        /// </summary>
        public void Pause()
        {
            mPaused = true;

            Fire("pause");
        }

        #endregion

        #region update

        /// <summary>
        /// Call this every so often
        /// </summary>
        /// <param name="pDeltaSeconds">time in seconds since update was last called</param>
        public void Update(double pDeltaSeconds)
        {
            if (mPaused)
                return;

            if (mTimed)
                mNextFrameCountdown -= pDeltaSeconds;
            else
                mNextFrameCountdown -= 1;

            mTimeElapsed += pDeltaSeconds;

            while (mNextFrameCountdown < 0)
            {
                if (mAnimation.MoveNext())
                {
                    // lol commenting because i used an xor and i don't want you to mistake it for an exponent
                    if (mTimed ^ mAnimation.Current >= 0)
                        mNextFrameCountdown = 0;

                    mTimed = mAnimation.Current >= 0;

                    // positive value denotes time, negative value denotes frames
                    if (mTimed)
                        mNextFrameCountdown += mAnimation.Current;
                    else
                        mNextFrameCountdown -= mAnimation.Current;
                }
                else
                {
                    mAnimation = null;
                    mPaused = true;
                    Flag("finished");
                    Fire("finished");
                    break;
                }
            }
        }

        #endregion

        #region protected members

        /// <summary>
        /// Derived classes can use this to fire named events at given times during playback
        /// </summary>
        /// <param name="pEvent">Name of event to fire</param>
        protected void Fire(string pEvent)
        {
            if (mEvents.ContainsKey(pEvent))
                mEvents[pEvent]();
        }

        /// <summary>
        /// Override with a semi-coroutine, yield returns number of seconds (positive double) until next frame
        /// or number of ticks (negative double) until next frame
        /// </summary>
        /// <returns></returns>
        protected abstract IEnumerable<double> Animate();

        #endregion
    }
}